﻿using System;
using System.Text;
using System.Collections.Generic;
using System.Linq;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using System.Threading;

namespace Ckknight.StateMachine.Test
{
    [TestClass]
    public class StateMachineTests
    {
        private static void AssertThrows<TException>(Action action) where TException : Exception
        {
            if (action == null)
            {
                throw new ArgumentNullException("action");
            }

            try
            {
                action();
            }
            catch (TException)
            {
                return;
            }

            Assert.Fail("Did not throw " + typeof(TException));
        }

        private static bool SetEqual<T>(IEnumerable<T> alpha, IEnumerable<T> bravo)
        {
            if (alpha == null)
            {
                throw new ArgumentNullException("alpha");
            }
            if (bravo == null)
            {
                throw new ArgumentNullException("bravo");
            }

            T[] alphaArray = alpha.ToArray();

            return alphaArray.Intersect(bravo).Count() == alphaArray.Length;
        }

        private class BooleanHolder
        {
            public BooleanHolder(bool initialValue)
            {
                _value = initialValue;
            }

            private bool _value;
            public void Set(bool value)
            {
                _value = value;
            }

            public static implicit operator bool(BooleanHolder holder)
            {
                if (holder == null)
                {
                    throw new ArgumentNullException("holder");
                }

                return holder._value;
            }
        }

        private void StandardCase<TState, TTrigger>(TState state1, TState state2, TState state3, TState badState, TTrigger trigger1, TTrigger trigger2, TTrigger trigger3, TTrigger badTrigger)
        {
            var builder = StateMachineBuilder.Create<TState, TTrigger>();

            builder.Configure(state1)
                .Permit(trigger1, state2);

            var machine = builder.CreateFactory().Generate(state1);

            Assert.AreEqual(state1, machine.State);
            Assert.IsTrue(machine.IsInState(state1));
            Assert.IsFalse(machine.IsInState(state2));
            Assert.IsFalse(machine.IsInState(state3));
            Assert.IsFalse(machine.IsInState(badState)); // should not error even though State does not exist

            Assert.IsTrue(machine.CanFire(trigger1));
            Assert.IsFalse(machine.CanFire(trigger2));
            Assert.IsFalse(machine.CanFire(trigger3));
            Assert.IsFalse(machine.CanFire(badTrigger));
            Assert.IsNotNull(machine.PermittedTriggers);
            Assert.IsTrue(new[] { trigger1 }.SequenceEqual(machine.PermittedTriggers));

            AssertThrows<ArgumentException>(() => machine.Fire(trigger2));
            AssertThrows<ArgumentException>(() => machine.Fire(badTrigger));

            machine.Fire(trigger1);

            Assert.AreEqual(state2, machine.State);
            Assert.IsFalse(machine.IsInState(state1));
            Assert.IsTrue(machine.IsInState(state2));
            Assert.IsFalse(machine.IsInState(state3));
            Assert.IsFalse(machine.IsInState(badState)); // should not error even though State does not exist

            Assert.IsFalse(machine.CanFire(trigger1));
            Assert.IsFalse(machine.CanFire(trigger2));
            Assert.IsFalse(machine.CanFire(trigger3));
            Assert.IsFalse(machine.CanFire(badTrigger));
            Assert.IsNotNull(machine.PermittedTriggers);
            Assert.IsTrue(Enumerable.Empty<TTrigger>().SequenceEqual(machine.PermittedTriggers));
        }

        [TestMethod]
        public void StandardCase()
        {
            StandardCase(
                State.Alpha, State.Bravo, State.Charlie, (State)(-1),
                Trigger.Dog, Trigger.Emu, Trigger.Fox, (Trigger)(-1));

            StandardCase(
                "alpha", "bravo", "charlie", null,
                "dog", "emu", "fox", null);
            StandardCase(
                "alpha", "bravo", "charlie", "bad",
                "dog", "emu", "fox", "bad");

            StandardCase(
                new { x = "alpha" }, new { x = "bravo" }, new { x = "charlie" }, null,
                new { y = "dog" }, new { y = "emu" }, new { y = "fox" }, null);
            StandardCase(
                new { x = "alpha" }, new { x = "bravo" }, new { x = "charlie" }, new { x = "bad" },
                new { y = "dog" }, new { y = "emu" }, new { y = "fox" }, new { y = "bad" });
        }

        private void OnEntry<TState, TTrigger>(TState alpha, TState bravo, TTrigger trigger)
        {
            var builder = StateMachineBuilder.Create<TState, TTrigger>();

            builder.Configure(alpha)
                .Permit(trigger, bravo);

            bool[] onEntryFired = { false, false, false, false };
            builder.Configure(bravo)
                .OnEntry(() => onEntryFired[0] = true)
                .OnEntry(() => onEntryFired[1] = true)
                .OnEntry(() => onEntryFired[2] = true)
                .OnEntry(() => onEntryFired[3] = true);

            var machine = builder.CreateFactory().Generate(alpha);

            Assert.IsFalse(onEntryFired[0]);
            Assert.IsFalse(onEntryFired[1]);
            Assert.IsFalse(onEntryFired[2]);
            Assert.IsFalse(onEntryFired[3]);
            machine.Fire(trigger);
            Assert.IsTrue(onEntryFired[0]);
            Assert.IsTrue(onEntryFired[1]);
            Assert.IsTrue(onEntryFired[2]);
            Assert.IsTrue(onEntryFired[3]);
        }

        [TestMethod]
        public void OnEntry()
        {
            OnEntry(State.Alpha, State.Bravo, Trigger.Dog);
            OnEntry("alpha", "bravo", "dog");
            OnEntry(new { x = "alpha" }, new { x = "bravo" }, new { y = "dog" });
        }

        private void OnExit<TState, TTrigger>(TState alpha, TState bravo, TTrigger trigger)
        {
            var builder = StateMachineBuilder.Create<TState, TTrigger>();

            bool[] onExitFired = { false, false, false, false };
            builder.Configure(alpha)
                .Permit(trigger, bravo)
                .OnExit(() => onExitFired[0] = true)
                .OnExit(() => onExitFired[1] = true)
                .OnExit(() => onExitFired[2] = true)
                .OnExit(() => onExitFired[3] = true);

            var machine = builder.CreateFactory().Generate(alpha);

            Assert.IsFalse(onExitFired[0]);
            Assert.IsFalse(onExitFired[1]);
            Assert.IsFalse(onExitFired[2]);
            Assert.IsFalse(onExitFired[3]);
            machine.Fire(trigger);
            Assert.IsTrue(onExitFired[0]);
            Assert.IsTrue(onExitFired[1]);
            Assert.IsTrue(onExitFired[2]);
            Assert.IsTrue(onExitFired[3]);
        }

        [TestMethod]
        public void OnExit()
        {
            OnExit(State.Alpha, State.Bravo, Trigger.Dog);
            OnExit("alpha", "bravo", "dog");
            OnExit(new { x = "alpha" }, new { x = "bravo" }, new { y = "dog" });
        }

        private void OnEntryFrom<TState, TTrigger>(TState alpha, TState bravo, TTrigger[] triggers)
        {
            var builder = StateMachineBuilder.Create<TState, TTrigger>();

            var onEntryFired = new Dictionary<TTrigger, bool>();

            var alphaBuilder = builder.Configure(alpha);
            var bravoBuilder = builder.Configure(bravo)
                .Permit(triggers[0], alpha);
            foreach (var t in triggers)
            {
                onEntryFired[t] = false;
                Action<TTrigger> action = (trigger) =>
                {
                    alphaBuilder.Permit(trigger, bravo);
                    bravoBuilder
                        .OnEntryFrom(trigger, () => onEntryFired[trigger] = true);
                };
                action(t);
            }

            var machine = builder.CreateFactory().Generate(alpha);

            foreach (var t in triggers)
            {
                Assert.AreEqual(alpha, machine.State);
                foreach (var pair in onEntryFired)
                {
                    Assert.IsFalse(pair.Value);
                }
                machine.Fire(t);
                Assert.AreEqual(bravo, machine.State);
                foreach (var pair in onEntryFired)
                {
                    if (object.Equals(pair.Key, t))
                    {
                        Assert.IsTrue(pair.Value);
                    }
                    else
                    {
                        Assert.IsFalse(pair.Value);
                    }
                }
                onEntryFired[t] = false;
                machine.Fire(triggers[0]);
            }
        }

        [TestMethod]
        public void OnEntryFrom()
        {
            OnEntryFrom(State.Alpha, State.Bravo, new[] { Trigger.Dog, Trigger.Emu, Trigger.Fox });
            OnEntryFrom("alpha", "bravo", new[] { "a", "b", "c", "d", "e", "f", "g", "h" });
            OnEntryFrom(new { x = "alpha" }, new { x = "bravo" }, new[] { "a", "b", "c" }.Select(x => new { y = x }).ToArray());
        }

        public void OnExitTo<TState, TTrigger>(TState alpha, TState bravo, TTrigger[] triggers)
        {
            var builder = StateMachineBuilder.Create<TState, TTrigger>();

            var onExitFired = new Dictionary<TTrigger, bool>();

            var alphaBuilder = builder.Configure(alpha);
            var bravoBuilder = builder.Configure(bravo)
                .Permit(triggers[0], alpha);
            foreach (var t in triggers)
            {
                onExitFired[t] = false;
                Action<TTrigger> action = (trigger) =>
                {
                    alphaBuilder
                        .Permit(trigger, bravo)
                        .OnExitTo(trigger, () => onExitFired[trigger] = true);
                };
                action(t);
            }

            var machine = builder.CreateFactory().Generate(alpha);

            foreach (var t in triggers)
            {
                Assert.AreEqual(alpha, machine.State);
                foreach (var pair in onExitFired)
                {
                    Assert.IsFalse(pair.Value);
                }
                machine.Fire(t);
                Assert.AreEqual(bravo, machine.State);
                foreach (var pair in onExitFired)
                {
                    if (object.Equals(pair.Key, t))
                    {
                        Assert.IsTrue(pair.Value);
                    }
                    else
                    {
                        Assert.IsFalse(pair.Value);
                    }
                }
                onExitFired[t] = false;
                machine.Fire(triggers[0]);
            }
        }

        [TestMethod]
        public void OnExitTo()
        {
            OnExitTo(State.Alpha, State.Bravo, new[] { Trigger.Dog, Trigger.Emu, Trigger.Fox });
            OnExitTo("alpha", "bravo", new[] { "a", "b", "c", "d", "e", "f", "g", "h" });
            OnExitTo(new { x = "alpha" }, new { x = "bravo" }, new[] { "a", "b", "c" }.Select(x => new { y = x }).ToArray());
        }

        [TestMethod]
        public void Reentry()
        {
            var builder = StateMachineBuilder.Create<State, Trigger>();

            var onEntry = new BooleanHolder(false);
            var onExit = new BooleanHolder(false);
            var onEntryFrom = new BooleanHolder(false);
            var onExitTo = new BooleanHolder(false);
            
            builder.Configure(State.Alpha)
                .PermitReentry(Trigger.Dog)
                .OnEntry(() =>
                {
                    Assert.IsTrue(onExit);
                    Assert.IsTrue(onExitTo);
                    Assert.IsFalse(onEntry);
                    onEntry.Set(true);
                })
                .OnExit(() =>
                {
                    Assert.IsFalse(onEntry);
                    Assert.IsFalse(onEntryFrom);
                    Assert.IsFalse(onExit);
                    onExit.Set(true);
                })
                .OnEntryFrom(Trigger.Dog, () =>
                {
                    Assert.IsTrue(onExit);
                    Assert.IsTrue(onExitTo);
                    Assert.IsFalse(onEntryFrom);
                    onEntryFrom.Set(true);
                })
                .OnExitTo(Trigger.Dog, () =>
                {
                    Assert.IsFalse(onEntry);
                    Assert.IsFalse(onEntryFrom);
                    Assert.IsFalse(onExitTo);
                    onExitTo.Set(true);
                });

            var machine = builder.CreateFactory().Generate(State.Alpha);

            Assert.IsFalse(onEntry);
            Assert.IsFalse(onExit);
            Assert.IsFalse(onExitTo);
            Assert.IsFalse(onEntryFrom);
            machine.Fire(Trigger.Dog);
            Assert.IsTrue(onEntry);
            Assert.IsTrue(onExit);
            Assert.IsTrue(onExitTo);
            Assert.IsTrue(onEntryFrom);
        }

        [TestMethod]
        public void Ignored()
        {
            var builder = StateMachineBuilder.Create<State, Trigger>();

            builder.Configure(State.Alpha)
                .Ignore(Trigger.Dog)
                .OnEntry(() => Assert.Fail())
                .OnExit(() => Assert.Fail())
                .OnEntryFrom(Trigger.Dog, () => Assert.Fail())
                .OnExitTo(Trigger.Dog, () => Assert.Fail());

            var machine = builder.CreateFactory().Generate(State.Alpha);

            machine.Fire(Trigger.Dog);
        }

        [TestMethod]
        public void WithEqualityComparer()
        {
            var builder = StateMachineBuilder.Create<string, string>(StringComparer.OrdinalIgnoreCase, StringComparer.OrdinalIgnoreCase);

            builder.Configure("alpha")
                .Permit("VERB", "BRAVO");

            builder.Configure("bravo")
                .Permit("OTHER", "ALPHA");

            var machine = builder.CreateFactory().Generate("Alpha");

            Assert.IsTrue(StringComparer.OrdinalIgnoreCase.Equals(machine.State, "Alpha"));
            Assert.IsTrue(machine.IsInState("alpha"));
            Assert.IsTrue(machine.IsInState("Alpha"));
            Assert.IsTrue(machine.IsInState("ALPHA"));
            Assert.IsFalse(machine.IsInState("bravo"));
            Assert.IsFalse(machine.IsInState("Bravo"));
            Assert.IsFalse(machine.IsInState("BRAVO"));
            Assert.IsFalse(machine.IsInState(null));

            Assert.IsTrue(machine.CanFire("verb"));
            Assert.IsTrue(machine.CanFire("Verb"));
            Assert.IsTrue(machine.CanFire("VERB"));
            Assert.IsFalse(machine.CanFire("other"));
            Assert.IsFalse(machine.CanFire("Other"));
            Assert.IsFalse(machine.CanFire("OTHER"));
            Assert.IsFalse(machine.CanFire(null));

            var permittedTriggers = machine.PermittedTriggers;
            Assert.IsNotNull(permittedTriggers);
            Assert.AreEqual(1, permittedTriggers.Count());
            Assert.IsTrue(StringComparer.OrdinalIgnoreCase.Equals("Verb", permittedTriggers.Single()));

            AssertThrows<ArgumentException>(() => machine.Fire("Other"));
            AssertThrows<ArgumentException>(() => machine.Fire(null));

            machine.Fire("Verb");

            Assert.IsTrue(StringComparer.OrdinalIgnoreCase.Equals(machine.State, "Bravo"));
            Assert.IsFalse(machine.IsInState("alpha"));
            Assert.IsFalse(machine.IsInState("Alpha"));
            Assert.IsFalse(machine.IsInState("ALPHA"));
            Assert.IsTrue(machine.IsInState("bravo"));
            Assert.IsTrue(machine.IsInState("Bravo"));
            Assert.IsTrue(machine.IsInState("BRAVO"));
            Assert.IsFalse(machine.IsInState(null));

            Assert.IsFalse(machine.CanFire("verb"));
            Assert.IsFalse(machine.CanFire("Verb"));
            Assert.IsFalse(machine.CanFire("VERB"));
            Assert.IsTrue(machine.CanFire("other"));
            Assert.IsTrue(machine.CanFire("Other"));
            Assert.IsTrue(machine.CanFire("OTHER"));
            Assert.IsFalse(machine.CanFire(null));

            permittedTriggers = machine.PermittedTriggers;
            Assert.IsNotNull(permittedTriggers);
            Assert.AreEqual(1, permittedTriggers.Count());
            Assert.IsTrue(StringComparer.OrdinalIgnoreCase.Equals("Other", permittedTriggers.Single()));
        }

        private void SubstateOf<TState, TTrigger>(TState parent, TState child, TState otherState, TTrigger trigger1, TTrigger trigger2, TTrigger trigger3)
        {
            var builder = StateMachineBuilder.Create<TState, TTrigger>();

            var parentEntry = new BooleanHolder(false);
            var childEntry = new BooleanHolder(false);
            var parentExit = new BooleanHolder(false);
            var childExit = new BooleanHolder(false);

            builder.Configure(parent)
                .OnEntry(() =>
                {
                    Assert.IsFalse(parentEntry);
                    Assert.IsFalse(childEntry);
                    parentEntry.Set(true);
                })
                .OnExit(() =>
                {
                    Assert.IsFalse(parentExit);
                    parentExit.Set(true);
                })
                .PermitReentry(trigger1)
                .Permit(trigger2, otherState);

            builder.Configure(child)
                .OnEntry(() =>
                {
                    Assert.IsTrue(parentEntry);
                    Assert.IsFalse(childEntry);
                    childEntry.Set(true);
                })
                .OnExit(() =>
                {
                    Assert.IsFalse(childExit);
                    Assert.IsFalse(parentExit);
                    childExit.Set(true);
                })
                .SubstateOf(parent)
                .PermitReentry(trigger2)
                .Permit(trigger3, otherState);

            builder.Configure(otherState)
                .Permit(trigger1, child);

            var machine = builder.CreateFactory().Generate(parent);

            Assert.IsFalse(parentEntry);
            Assert.IsFalse(childEntry);
            Assert.IsFalse(parentExit);
            Assert.IsFalse(childExit);
            Assert.AreEqual(parent, machine.State);
            Assert.IsTrue(machine.IsInState(parent));
            Assert.IsFalse(machine.IsInState(child));
            Assert.IsFalse(machine.IsInState(otherState));
            Assert.IsTrue(SetEqual(machine.PermittedTriggers, new[] { trigger1, trigger2 }));

            machine.Fire(trigger1);
            Assert.AreEqual(parent, machine.State);
            Assert.IsTrue(parentEntry);
            Assert.IsFalse(childEntry);
            Assert.IsTrue(parentExit);
            Assert.IsFalse(childExit);
            parentEntry.Set(false);
            parentExit.Set(false);

            machine.Fire(trigger2);
            Assert.AreEqual(otherState, machine.State);
            Assert.IsFalse(machine.IsInState(parent));
            Assert.IsFalse(machine.IsInState(child));
            Assert.IsTrue(machine.IsInState(otherState));
            Assert.IsTrue(SetEqual(machine.PermittedTriggers, new[] { trigger1 }));
            Assert.IsFalse(parentEntry);
            Assert.IsFalse(childEntry);
            Assert.IsTrue(parentExit);
            Assert.IsFalse(childExit);
            parentExit.Set(false);

            machine.Fire(trigger1);
            Assert.AreEqual(child, machine.State);
            Assert.IsTrue(machine.IsInState(parent));
            Assert.IsTrue(machine.IsInState(child));
            Assert.IsFalse(machine.IsInState(otherState));
            Assert.IsTrue(SetEqual(machine.PermittedTriggers, new[] { trigger1, trigger2, trigger3 }));
            Assert.IsTrue(parentEntry);
            Assert.IsTrue(childEntry);
            Assert.IsFalse(parentExit);
            Assert.IsFalse(childExit);
            parentEntry.Set(false);
            childEntry.Set(false);

            machine.Fire(trigger1); // reentry based on the parent
            Assert.AreEqual(child, machine.State); // the child still, not the parent
            Assert.IsTrue(parentEntry);
            Assert.IsTrue(childEntry);
            Assert.IsTrue(parentExit);
            Assert.IsTrue(childExit);
            parentEntry.Set(false);
            childEntry.Set(false);
            parentExit.Set(false);
            childExit.Set(false);

            machine.Fire(trigger2); // reentry based on the child, overrides the transition to otherState
            Assert.AreEqual(child, machine.State); // still the child, obviously
            Assert.IsTrue(parentEntry);
            Assert.IsTrue(childEntry);
            Assert.IsTrue(parentExit);
            Assert.IsTrue(childExit);
            parentEntry.Set(false);
            childEntry.Set(false);
            parentExit.Set(false);
            childExit.Set(false);

            machine.Fire(trigger3);
            Assert.AreEqual(otherState, machine.State);
        }

        [TestMethod]
        public void SubstateOf()
        {
            SubstateOf(State.Alpha, State.Bravo, State.Charlie, Trigger.Dog, Trigger.Emu, Trigger.Fox);
            SubstateOf("alpha", "bravo", "charlie", "dog", "emu", "fox");
            SubstateOf(new { x = "alpha" }, new { x = "bravo" }, new { x = "charlie" }, new { y = "dog" }, new { y = "emu" }, new { y = "fox" });
        }

        private void SubstateOfEntryFrom<TState, TTrigger>(TState parent, TState child, TState otherState, TTrigger trigger1, TTrigger trigger2, TTrigger trigger3)
        {
            var builder = StateMachineBuilder.Create<TState, TTrigger>();

            var parentEntry = new BooleanHolder(false);
            var childEntry = new BooleanHolder(false);
            var parentExit = new BooleanHolder(false);
            var childExit = new BooleanHolder(false);

            builder.Configure(parent)
                .Permit(trigger1, parent)
                .Permit(trigger2, child)
                .Permit(trigger3, otherState);

            builder.Configure(child)
                .SubstateOf(parent);

            foreach (var trigger in new[] { trigger1, trigger2, trigger3 })
            {
                builder.Configure(parent)
                    .OnEntryFrom(trigger, () =>
                    {
                        Assert.IsFalse(parentEntry);
                        Assert.IsFalse(childEntry);
                        parentEntry.Set(true);
                    })
                    .OnExitTo(trigger, () =>
                    {
                        Assert.IsFalse(parentExit);
                        parentExit.Set(true);
                    });

                builder.Configure(child)
                    .OnEntryFrom(trigger, () =>
                    {
                        Assert.IsTrue(parentEntry);
                        Assert.IsFalse(childEntry);
                        childEntry.Set(true);
                    })
                    .OnExitTo(trigger, () =>
                    {
                        Assert.IsFalse(parentExit);
                        Assert.IsFalse(childExit);
                        childExit.Set(true);
                    });
            }

            builder.Configure(otherState)
                .Permit(trigger1, parent)
                .Permit(trigger2, child);

            var machine = builder.CreateFactory().Generate(parent);

            Assert.AreEqual(parent, machine.State);
            Assert.IsFalse(parentEntry);
            Assert.IsFalse(parentExit);
            Assert.IsFalse(childEntry);
            Assert.IsFalse(childExit);

            machine.Fire(trigger3);
            Assert.AreEqual(otherState, machine.State);
            Assert.IsFalse(parentEntry);
            Assert.IsTrue(parentExit);
            Assert.IsFalse(childEntry);
            Assert.IsFalse(childExit);
            parentExit.Set(false);

            machine.Fire(trigger1);
            Assert.AreEqual(parent, machine.State);
            Assert.IsTrue(parentEntry);
            Assert.IsFalse(parentExit);
            Assert.IsFalse(childEntry);
            Assert.IsFalse(childExit);
            parentEntry.Set(false);

            machine.Fire(trigger2);
            Assert.AreEqual(child, machine.State);
            Assert.IsTrue(parentEntry);
            Assert.IsTrue(parentExit);
            Assert.IsTrue(childEntry);
            Assert.IsFalse(childExit);
            parentEntry.Set(false);
            parentExit.Set(false);
            childEntry.Set(false);

            machine.Fire(trigger1);
            Assert.AreEqual(parent, machine.State);
            Assert.IsTrue(parentEntry);
            Assert.IsTrue(parentExit);
            Assert.IsFalse(childEntry);
            Assert.IsTrue(childExit);
            parentEntry.Set(false);
            parentExit.Set(false);
            childExit.Set(false);
        }

        [TestMethod]
        public void SubstateOfEntryFrom()
        {
            SubstateOfEntryFrom(State.Alpha, State.Bravo, State.Charlie, Trigger.Dog, Trigger.Emu, Trigger.Fox);
            SubstateOfEntryFrom("alpha", "bravo", "charlie", "dog", "emu", "fox");
            SubstateOfEntryFrom(new { x = "alpha" }, new { x = "bravo" }, new { x = "charlie" }, new { y = "dog" }, new { y = "emu" }, new { y = "fox" });
        }

        private void OnUnhandledTrigger<TState, TTrigger>(TState state, TTrigger trigger)
        {
            var gotUnhandledTrigger = new BooleanHolder(false);
            var builder = StateMachineBuilder.Create<TState, TTrigger>()
                .OnUnhandledTrigger(t =>
                {
                    Assert.AreEqual(t.PreviousState, state);
                    Assert.AreEqual(t.Trigger, trigger);
                    Assert.IsFalse(gotUnhandledTrigger);
                    gotUnhandledTrigger.Set(true);
                });

            var machine = builder.CreateFactory().Generate(state);

            Assert.IsFalse(gotUnhandledTrigger);
            machine.Fire(trigger);
            Assert.IsTrue(gotUnhandledTrigger);
        }

        [TestMethod]
        public void OnUnhandledTrigger()
        {
            OnUnhandledTrigger(State.Alpha, Trigger.Dog);
            OnUnhandledTrigger("alpha", "dog");
            OnUnhandledTrigger(new { x = "alpha" }, new { y = "dog" });
        }

        private void MultipleMachines<TState, TTrigger>(TState alpha, TState bravo, TTrigger trigger, int count)
        {
            var builder = StateMachineBuilder.Create<TState, TTrigger>();

            builder.Configure(alpha)
                .Permit(trigger, bravo);

            builder.Configure(bravo)
                .Permit(trigger, alpha);

            var factory = builder.CreateFactory();

            var machines = Enumerable.Range(0, count)
                .Select(i => factory.Generate(alpha))
                .ToArray();

            Assert.AreEqual(machines.Distinct().Count(), machines.Length);

            foreach (var machine in machines)
            {
                Assert.AreEqual(alpha, machine.State);
                machine.Fire(trigger);
                Assert.AreEqual(bravo, machine.State);
            }

            foreach (var machine in machines)
            {
                Assert.AreEqual(bravo, machine.State);
                machine.Fire(trigger);
                Assert.AreEqual(alpha, machine.State);
            }
        }

        [TestMethod]
        public void MultipleMachines()
        {
            MultipleMachines(State.Alpha, State.Bravo, Trigger.Dog, 5);
            MultipleMachines("alpha", "bravo", "dog", 100);
            MultipleMachines(new { x = "alpha" }, new { x = "bravo" }, new { y = "dog" }, 1000);
        }

        private void Models<TState, TTrigger, TModel>(TState alpha, TState bravo, TTrigger trigger, TModel[] models)
        {
            var builder = StateMachineBuilder.Create<TState, TTrigger, TModel>();

            var alphaEntry = new List<TModel>();
            var bravoEntry = new List<TModel>();

            builder.Configure(alpha)
                .Permit(trigger, bravo)
                .OnEntry(t => alphaEntry.Add(t.Model));

            builder.Configure(bravo)
                .Permit(trigger, alpha)
                .OnEntry(t => bravoEntry.Add(t.Model));

            var factory = builder.CreateFactory();

            var machines = models
                .Select(m => factory.Generate(m, alpha))
                .ToArray();

            Assert.AreEqual(machines.Distinct().Count(), machines.Length);

            foreach (var machine in machines)
            {
                Assert.AreEqual(alpha, machine.State);
                machine.Fire(trigger);
                Assert.AreEqual(bravo, machine.State);

                Assert.AreEqual(1, bravoEntry.Count());
                Assert.AreEqual(machine.Model, bravoEntry.Single());
                bravoEntry.Clear();
            }

            foreach (var machine in machines)
            {
                Assert.AreEqual(bravo, machine.State);
                machine.Fire(trigger);
                Assert.AreEqual(alpha, machine.State);

                Assert.AreEqual(1, alphaEntry.Count());
                Assert.AreEqual(machine.Model, alphaEntry.Single());
                alphaEntry.Clear();
            }
        }

        [TestMethod]
        public void Models()
        {
            Models(State.Alpha, State.Bravo, Trigger.Dog, new[] { 1, 2, 3, 4, 5 });
            Models("alpha", "bravo", "dog", new[] { "a", "b", "c", "d", "e" });
            Models(new { x = "alpha" }, new { x = "bravo" }, new { y = "dog" }, new[] { new { z = 1 }, new { z = 2 }, new { z = 3 } });
        }


        private class MultithreadedDataModel
        {
            public int EnteredAlphaCount;
            public int ExitedAlphaCount;
            public int EnteredBravoCount;
            public int ExitedBravoCount;
        }
        private void MultipleMachinesMultithreaded<TState, TTrigger>(TState alpha, TState bravo, TTrigger trigger, int count, int cycles)
        {
            var builder = StateMachineBuilder.Create<TState, TTrigger, MultithreadedDataModel>();

            builder.Configure(alpha)
                .Permit(trigger, bravo)
                .OnEntry(t => t.Model.EnteredAlphaCount++)
                .OnExit(t => t.Model.ExitedAlphaCount++);

            builder.Configure(bravo)
                .Permit(trigger, alpha)
                .OnEntry(t => t.Model.EnteredBravoCount++)
                .OnExit(t => t.Model.ExitedBravoCount++);

            var factory = builder.CreateFactory();

            var randoms = new Random[count];
            randoms[0] = new Random();
            for (int i = 1; i < count; i++)
            {
                randoms[i] = new Random(randoms[0].Next());
            }
            var models = new MultithreadedDataModel[count];
            var threads = new Thread[count];
            for (int i = 0; i < count; i++)
            {
                Action<Random, MultithreadedDataModel> action = (random, model) =>
                {
                    threads[i] = new Thread(() =>
                    {
                        Thread.Sleep(random.Next(30, 50));
                        var machine = factory.Generate(model, alpha);

                        Assert.AreEqual(0, model.EnteredAlphaCount);
                        Assert.AreEqual(0, model.ExitedAlphaCount);
                        Assert.AreEqual(0, model.EnteredBravoCount);
                        Assert.AreEqual(0, model.ExitedBravoCount);

                        for (int j = 0; j < cycles; j++)
                        {
                            Thread.Sleep(random.Next(0, 5));

                            Assert.AreEqual(alpha, machine.State);
                            machine.Fire(trigger);
                            Assert.AreEqual(bravo, machine.State);

                            Assert.AreEqual(j, model.EnteredAlphaCount);
                            Assert.AreEqual(j + 1, model.ExitedAlphaCount);
                            Assert.AreEqual(j + 1, model.EnteredBravoCount);
                            Assert.AreEqual(j, model.ExitedBravoCount);

                            Thread.Sleep(random.Next(0, 5));

                            Assert.AreEqual(bravo, machine.State);
                            machine.Fire(trigger);
                            Assert.AreEqual(alpha, machine.State);

                            Assert.AreEqual(j + 1, model.EnteredAlphaCount);
                            Assert.AreEqual(j + 1, model.ExitedAlphaCount);
                            Assert.AreEqual(j + 1, model.EnteredBravoCount);
                            Assert.AreEqual(j + 1, model.ExitedBravoCount);
                        }
                    });
                };
                models[i] = new MultithreadedDataModel();
                action(randoms[i], models[i]);
            }

            for (int i = 0; i < count; i++)
            {
                threads[i].Start();
            }

            for (int i = 0; i < count; i++)
            {
                threads[i].Join();
                Assert.AreEqual(cycles, models[i].EnteredAlphaCount);
                Assert.AreEqual(cycles, models[i].ExitedAlphaCount);
                Assert.AreEqual(cycles, models[i].EnteredBravoCount);
                Assert.AreEqual(cycles, models[i].ExitedBravoCount);
            }
        }

        [TestMethod]
        public void MultipleMachinesMultithreaded()
        {
            MultipleMachinesMultithreaded(State.Alpha, State.Bravo, Trigger.Dog, 5, 20);
            MultipleMachinesMultithreaded("alpha", "bravo", "dog", 100, 10);
            MultipleMachinesMultithreaded(new { x = "alpha" }, new { x = "bravo" }, new { y = "dog" }, 1000, 10);
        }

        private void DynamicMethods<TState, TTrigger>(TState alpha, TState bravo, TState charlie, TTrigger trigger)
        {
            var builder = StateMachineBuilder.Create<TState, TTrigger>();

            int index = 0;
            var states = new[] { alpha, bravo, charlie };

            foreach (var state in states)
            {
                builder.Configure(state)
                    .Permit(trigger, () =>
                    {
                        index = (index + 1) % 3;
                        return states[index];
                    });
            }

            var machine = builder.CreateFactory().Generate(alpha);

            for (int i = 0; i < 10; i++)
            {
                Assert.AreEqual(alpha, machine.State);

                machine.Fire(trigger);

                Assert.AreEqual(bravo, machine.State);

                machine.Fire(trigger);

                Assert.AreEqual(charlie, machine.State);

                machine.Fire(trigger);
            }

            Assert.AreEqual(alpha, machine.State);
        }

        [TestMethod]
        public void DynamicMethods()
        {
            DynamicMethods(State.Alpha, State.Bravo, State.Charlie, Trigger.Dog);
            DynamicMethods("alpha", "bravo", "charlie", Trigger.Dog);
            DynamicMethods(new { x = "alpha" }, new { x = "bravo" }, new { x = "charlie" }, new { y = "dog" });
        }

        private void PermitIf<TState, TTrigger>(TState alpha, TState bravo, TTrigger trigger)
        {
            var builder = StateMachineBuilder.Create<TState, TTrigger>();

            var allow = new BooleanHolder(false);

            builder.Configure(alpha)
                .PermitIf(trigger, bravo, t => allow);

            var machine = builder.CreateFactory().Generate(alpha);

            Assert.AreEqual(alpha, machine.State);

            machine.Fire(trigger);

            Assert.AreEqual(alpha, machine.State);

            allow.Set(true);
            machine.Fire(trigger);

            Assert.AreEqual(bravo, machine.State);
        }

        [TestMethod]
        public void PermitIf()
        {
            PermitIf(State.Alpha, State.Bravo, Trigger.Dog);
            PermitIf("alpha", "bravo", Trigger.Dog);
            PermitIf(new { x = "alpha" }, new { x = "bravo" }, new { y = "dog" });
        }

        private void DynamicPermitIf<TState, TTrigger>(TState alpha, TState bravo, TTrigger trigger)
        {
            var builder = StateMachineBuilder.Create<TState, TTrigger>();

            var allow = new BooleanHolder(false);

            builder.Configure(alpha)
                .PermitIf(trigger, t => bravo, t => allow);

            var machine = builder.CreateFactory().Generate(alpha);

            Assert.AreEqual(alpha, machine.State);

            machine.Fire(trigger);

            Assert.AreEqual(alpha, machine.State);

            allow.Set(true);
            machine.Fire(trigger);

            Assert.AreEqual(bravo, machine.State);
        }

        [TestMethod]
        public void DynamicPermitIf()
        {
            DynamicPermitIf(State.Alpha, State.Bravo, Trigger.Dog);
            DynamicPermitIf("alpha", "bravo", "dog");
            DynamicPermitIf(new { x = "alpha" }, new { x = "bravo" }, new { y = "dog" });
        }

        private void TriggerParameters<TState, TTrigger, TArg>(TState alpha, TState bravo, TTrigger trigger, TArg x, TArg y)
        {
            var builder = StateMachineBuilder.Create<TState, TTrigger>()
                .SetTriggerParameters<TArg>(trigger);

            foreach (var state in new[] { alpha, bravo })
            {
                AssertThrows<ArgumentException>(() => builder.Configure(state)
                    .Permit(trigger, t => alpha));

                builder.Configure(state)
                    .Permit<TArg>(trigger, t =>
                    {
                        return object.Equals(t.Item1, x)
                            ? alpha
                            : bravo;
                    });
            }

            var machine = builder.CreateFactory().Generate(alpha);

            Assert.AreEqual(alpha, machine.State);

            AssertThrows<ArgumentException>(() => machine.Fire(trigger));
            AssertThrows<ArgumentException>(() => machine.Fire(trigger, x, y));

            machine.Fire(trigger, x);

            Assert.AreEqual(alpha, machine.State);

            machine.Fire(trigger, y);

            Assert.AreEqual(bravo, machine.State);

            machine.Fire(trigger, y);

            Assert.AreEqual(bravo, machine.State);

            machine.Fire(trigger, x);

            Assert.AreEqual(alpha, machine.State);
        }

        [TestMethod]
        public void TriggerParameters()
        {
            TriggerParameters(State.Alpha, State.Bravo, Trigger.Dog, "x", "y");
            TriggerParameters("alpha", "bravo", "dog", "x", "y");
        }
    }
}
